4.2 参数
指针阅读好文:
Go对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。调用时,必须按签名顺序传递指定类型和数量的实参,就算以“_”命名的参数也不能忽略。
在参数列表中,相邻的同类型参数可合并。
func test(x,y int,s string, _bool) *int{
return nil
}
func main() {
test(1,2, "abc") // 错误:not enough arguments in call to test
}
参数可视作函数局部变量,因此不能在相同层次定义同名变量。
func add(x,y int)int{
x:=100 // 错误:no new variables on left side of:=
var y int // 错误:y redeclared in this block
return x+y
}形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数等。
不管是指针、引用类型,还是其他类型参数,都是值拷贝传递(pass-by-value)。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
func test(x*int) {
fmt.Printf("pointer: %p,target: %v\n", &x,x) // 输出形参x的地址
}
func main() {
a:=0x100
p:= &a
fmt.Printf("pointer: %p,target: %v\n", &p,p) // 输出实参p的地址
test(p)
}输出:
pointer:0xc82002c020,target:0xc82000a298
pointer:0xc82002c030,target:0xc82000a298
从输出结果可以看出,尽管实参和形参都指向同一目标,但传递指针时依然被复制。
表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
其实在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快得多。另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好。
下面是一个指针参数导致实参变量被分配到堆上的简单示例。可对比传值参数的汇编代码,从中可看出具体的差别。
func test(p*int) {
go func() { // 延长p生命周期
println(p)
}()
}
func main() {
x:=100
p:= &x
test(p)
}
输出:
$go build-gcflags"-m" // 输出编译器优化策略
moved to heap:x
&x escapes to heap // 逃逸
$go tool objdump-s"main\.main"test
TEXT main.main(SB)test.go
CALL runtime.newobject(SB) // 在堆上为x分配内存
CALL main.test(SB)
要实现传出参数(out),通常建议使用返回值。当然,也可继续用二级指针。
func test(p**int) {
x:=100
*p= &x
}
func main() {
var p*int
test(&p)
println(*p)
}
输出:
100
如果函数参数过多,建议将其重构为一个复合结构类型,也算是变相实现可选参数和命名实参功能。
type serverOption struct{
address string
port int
path string
timeout time.Duration
log *log.Logger
}
func newOption() *serverOption{
return &serverOption{ // 默认参数
address: "0.0.0.0",
port: 8080,
path: "/var/test",
timeout:time.Second*5,
log: nil,
}
}
func server(option *serverOption) {}
func main() {
opt:=newOption()
opt.port=8085 // 命名参数设置
server(opt)
}将过多的参数独立成option struct,既便于扩展参数集,也方便通过newOption函数设置默认配置。这也是代码复用的一种方式,避免多处调用时烦琐的参数配置。
变参
变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。
func test(s string,a...int) {
fmt.Printf("%T, %v\n",a,a) // 显示类型和值
}
func main() {
test("abc",1,2,3,4)
}输出:
[]int, [1 2 3 4]
将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。
func test(a...int) {
fmt.Println(a)
}
func main() {
a:= [3]int{10,20,30}
test(a[:]...) // 转换为slice后展开
}既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数copy复制底层数据。
func test(a...int) {
for i:=range a{
a[i] +=100
}
}
func main() {
a:= []int{10,20,30}
test(a...)
fmt.Println(a)
}
输出:
[110 120 130]